from persistent.mapping import PersistentMapping
from datetime import datetime
import urllib2
import jsonlib2

# Types that should be searchable
AVAILABLE_TYPES = [
        {'name':'bus_stop', 'title':'Bus Stops', 'icon':'bus'},
        #{'name':'carertoilets', 'title':'Carer friendly toilets', 'icon':'toilet'},
        {'name':'park', 'title':'Parks', 'icon':'park'},
        {'name':'pub', 'title':'Pubs', 'icon':'pub'},
        {'name':'hospital', 'title':'Hospitals', 'icon':'hosp'},
        {'name':'pharmacy', 'title':'Pharmacies', 'icon':'hosp'},
        {'name':'parking', 'title':'Car Parks', 'icon':'hosp'},
        {'name':'station', 'title':'Train Stations', 'icon':'hosp'}
]
# Add trains
# 'bus_stop':busIcon, 'carertoilet':toiletIcon, 'park':parkIcon, 'hospital':hospitalIcon, 'pub':pubIcon

# Radius in which to carry out the search in metres
SEARCH_RADIUS = 100

# Make some dummy data for use in proving the concept and so that Ant has something to work with
# These all need to be somewhere near lat: 51.53510, long: -0.12185 ... as that is where the 
# Guardian lives!
SAMPLE_DATA = {
        'bus_stop':[
            {'lat': 51.53400, 'lon': -0.12555, 'name': 'This is a test Bus Stop'},
            {'lat': 51.53601, 'lon': -0.11871, 'name': 'This is a test Bus Stop'},
            {'lat': 51.53419, 'lon': -0.11891, 'name': 'This is a test Bus Stop'}
        ],
        'park':[
            {'lat': 51.53312, 'lon': -0.12987, 'name': 'This is a test Park'},
            {'lat': 51.53323, 'lon': -0.12653, 'name': 'This is a test Park'}
        ],
        'pub':[
            {'lat': 51.53734, 'lon': -0.12266, 'name': 'This is a test Pub'}
        ],
        'hospital':[
            {'lat': 51.53295, 'lon': -0.12100, 'name': 'This is a test Hospital'}
        ],
        'pharmacy':[
            {'lat': 51.53630, 'lon': -0.11985, 'name': 'This is a test Pharmacy'}
        ]
    }

class ItemMetaComments(object):
    def __init__(self, itemtype, lat, lon):
        self.item_type = self._setType(itemtype)
        self.lat = lat
        self.lon = lon
        self.comments = ()
    
    def _setType(self, itemtype):
        if itemtype in [x['name'] for x in AVAILABLE_TYPES]:
            self.item_type = itemtype
        else:
            self.item_type = "NONE"
    
    def addComment(self, comment):
        comments = list(self.comments)
        comments.append({'comment':comment, 'date':datetime.now()})
        self.comments = tuple(comments)
    
    def getComments(self):
        return self.comments

class ItemMetaRating(object):
    """ This is an object representation of an items rating that should be displayed on a map """
    def __init__(self, itemtype, lat, lon, acc_rating={'up':0, 'down':0}, nice_rating={'up':0, 'down':0}):
        self.item_type = self._setType(itemtype)
        self.lat = lat
        self.lon = lon
        self.acc_rating = acc_rating
        self.nice_rating = nice_rating
    
    def _setType(self, itemtype):
        if itemtype in [x['name'] for x in AVAILABLE_TYPES]:
            self.item_type = itemtype
        else:
            self.item_type = "NONE"
    
    def _getPerc(self, ratings, key):
        """ Returns the percentage of the specified key (up or down) """
        return ratings[key] / (ratings['up'] + ratings['down']) * 100
    
    def getFormattedRatings(self):
        """ Cheating function for speed of development and working with JSON in a very amaturish way """
        # TODO: do this properly
        acc = self.getAccRating()
        nice = self.getNiceRating()
        nicestring = {
            'niceup': nice['good'],
            'nicedown': nice['bad'],
            'nicetotal': nice['total'],
            'accup': acc['good'],
            'accdown': acc['bad'],
            'acctotal': acc['total']
        }
        return nicestring
    
    def getAccRating(self):
        # Nice to have: percentages instead, but for times sake, not at the moment
        #percG = self._getPerc(self.acc_rating, 'up')
        #percB = self._getPerc(self.acc_rating, 'down')
        return {'good':self.acc_rating['up'], 'bad':self.acc_rating['down'], 'total': self.acc_rating['up'] + self.acc_rating['down']}
    
    def getNiceRating(self):
        # Nice to have: percentages instead, but for times sake, not at the moment
        #percG = self._getPerc(self.nice_rating, 'up')
        #percB = self._getPerc(self.nice_rating, 'down')
        return {'good':self.nice_rating['up'], 'bad':self.nice_rating['down'], 'total': self.nice_rating['up'] + self.nice_rating['down']}
    


class MyModel(PersistentMapping):
    __parent__ = __name__ = None
    
    def _initialiseSite(self):
        print "Initialising the Bristol Bus route data from a CSV file"
        # self._busInfo = csv_reader()
        # You always have to do this unless you are changing things in Persistent Dicts
        self._metaratings = ()
        # Lets set up a fake rating shall we, on a specific item
        for i in range(200): # Add some positive ratings to a random item
            #rating, rating_type, item=None, itemId=None :: up/down and acc/nice
            self.addItemMetaRating("up", "acc", itemId="51.53591:-0.13144:hospital")
        # and some -ive ones
        for i in range(15): # Add some positive ratings to a random item
            #rating, rating_type, item=None, itemId=None :: up/down and acc/nice
            self.addItemMetaRating("down", "acc", itemId="51.53591:-0.13144:hospital")
        # and some +ive nice
        for i in range(150): # Add some positive ratings to a random item
            #rating, rating_type, item=None, itemId=None :: up/down and acc/nice
            self.addItemMetaRating("up", "nice", itemId="51.53591:-0.13144:hospital")
        # and some -ive nice
        for i in range(100): # Add some positive ratings to a random item
            #rating, rating_type, item=None, itemId=None :: up/down and acc/nice
            self.addItemMetaRating("down", "nice", itemId="51.53591:-0.13144:hospital")
            
        self._metacomments = ()
        self._previousSearches = ()
        import transaction
        transaction.commit()
    
    def _performAPISearch(self, lat, lon, item_type):
        # The array we will eventually return
        returns = []
        # Performs the search via. the Cloudmade API
        args = {
            'apikey':"26839c8f5b864c9f839a25cebb01f052",
            'item_type': item_type,
            'lat': str(lat),
            'lon': str(lon),
            'radius': "1000",   # in metres
            'maxresults': "10"  # This is the default set in the Cloudmade examples, and as such may well need adjusting
        }
        
        urlr = "http://geocoding.cloudmade.com/%(apikey)s/geocoding/v2/find.js?object_type=%(item_type)s&around=%(lat)s,%(lon)s&distance=%(radius)s&results=%(maxresults)s" % args
        req = urllib2.urlopen(urlr)
        #import pdb; pdb.set_trace( )
        response = req.read()
        
        # Turn the returned text into a structure theat Python can process... It looks like this:
        """
            { bounds': [[51.532260000000001, -0.12634999999999999],
                        [51.532260000000001, -0.12634999999999999]],
             'crs': {'properties': {'code': 4326, 'coordinate_order': [0, 1]},
                     'type': 'EPSG'},
             'features': [{'bounds': [[51.532260000000001, -0.12634999999999999],
                                      [51.532260000000001, -0.12634999999999999]],
                           'centroid': {'coordinates': [51.532260000000001,
                                                        -0.12634999999999999],
                                        'type': 'POINT'},
                           'id': 31985892,
                           'properties': {'amenity': 'pharmacy',
                                          'name': 'Boots',
                                          'osm_element': 'node',
                                          'osm_id': '647045756'},
                           'type': 'Feature'}],
             'found': 1,
             'type': 'FeatureCollection'}
        """
        items = jsonlib2.read(response)
        # Items can occasionally be empty
        if items:
            numberfound = items["found"] # Don't need this right now but could be useful in the future 
            results = items["features"] # These are the actual located points
            for result in results:
                #print result["centroid"]["type"]
                if result["centroid"]["type"] != "POINT":
                    print "\nI don't know what to do with this result because it is not a POINT"
                    continue # Skip it
                
                point = result["centroid"]["coordinates"] # This is an array like [lat, long]
                name = result["properties"].get("name", result["properties"].get("synthesized_name", "Unnamed"))
                
                # Add this result to the results
                returns.append({'lat':point[0], 'lon':point[1], 'name':name})
            
            return returns
        
        return []
    
    def _performLookup(self, lat, lon):
        # Performs a search of the various sources we want to look up from, in a given radius
        # This ALWAYS looks up everything, so that in the future we can cache entire result sets
        # in the system to ease up on requests
        print "performing an actual lookup now"
        results = {}
        for search in AVAILABLE_TYPES:
            # Perform the search by the search term
            # TODO TODO TODO!!!
            results[search['name']] = self._performAPISearch(lat, lon, search['name'])
        
        # Return REAL data
        return results
    
        
    def performSearch(self, lat, lon, item_type="all"):
        """ Performs a search for items within a specified radius of a given lat/long """
        prev_searches = list(self._previousSearches)
        # Look through previous searches to see whether this location has been searched before
        returns = []
        results = {}
        exists = False
        for idx, search in enumerate(prev_searches):
            if search["lon"] == lon and search["lat"] == lat:
                results = search
                exists = True
                
        # If we couldn't find anything, return the results from a new search
        if not exists:
            results = self._performLookup(lat, lon)
        
        #print results
        
        # Then we need to look through the results and build up the thing we want to return
        if item_type == "all":
            for key in results:
                for item in results[key]:
                    # TODO: Separate the ratings so they too can be obtained aSynchronously and on demand.
                    #       I need BETTER jQuery skills
                    itemId = ":".join([str(item['lat']), str(item['lon']), key])
                    ratings = self.getItemMetaRatings(itemId=itemId)
                    if not ratings:
                        ratings = {'accup': 0, 'accdown': 0, 'nicetotal': 0, 'nicedown': 0, 'niceup': 0, 'acctotal': 0}
                    else:
                        ratings = ratings['obj'].getFormattedRatings()
                    info = {'lat':item["lat"], 'lon':item["lon"], 'type':key, 'name':item["name"]}
                    info.update(ratings)
                    returns.append(info)
        else:
            if item_type in [x['name'] for x in AVAILABLE_TYPES]:
                # TODO: Separate the ratings so they too can be obtained aSynchronously and on demand.
                #       I need BETTER jQuery skills
                for item in results[item_type]:
                    itemId = ":".join([str(item['lat']), str(item['lon']), item_type])
                    ratings = self.getItemMetaRatings(itemId=itemId)
                    if not ratings:
                        ratings = {'accup': 0, 'accdown': 0, 'nicetotal': 0, 'nicedown': 0, 'niceup': 0, 'acctotal': 0}
                    else:
                        ratings = ratings['obj'].getFormattedRatings()
                    info = {'lat':item["lat"], 'lon':item["lon"], 'type':item_type, 'name':item["name"]}
                    info.update(ratings)
                    returns.append(info)
        
        # TODO: Store the results and some kind of time based dirty flag
        #       BUT NOT YET BABY, NOT YET
        return jsonlib2.write(returns)
        
        self._previousSearches = tuple(prev_searches)
        import transaction
        transaction.commit()
    
    def _getRatingOrComments(self, item=None, itemId=None, which=None):
        properties = {'ratings':'_metaratings', 'comments':'_metacomments'}
        if which not in properties.keys():
            print "returning from which"
            return []
        if not item and not itemId:
            print "returning cuz of no item id or item"
            return []
        
        if item and not itemId:
            itemId = "%f:%f:%s" % (item.get('lat', "n"), item.get('lon', "n"), item.get('item_type', "n"))
        
        # If the id is not valid
        if "n" in itemId.split(":"):
            return []
        
        # Now return the metaratings for the item in question
        metadata = []
        if which == 'ratings':
            metadata = self._metaratings
        elif which == 'comments':
            metadata = self._metacomments
        
        for idx, r in enumerate([x["id"] for x in metadata]):
            if itemId == r:
                return self._metaratings[idx]
        # Finally return empty if nothing was found
        return []
    
    def getItemMetaRatings(self, item=None, itemId=None):
        ratings = self._getRatingOrComments(item=item, itemId=itemId, which="ratings")
        if not ratings:
            # Return a fake formatted string due to the fact that I'm no longer 
            # collecting ratings aSynchronously by themselves
            return False
        return ratings
        
    
    def getItemMetaComments(self, item=None, itemId=None):
        return self._getRatingOrComments(item=item, itemId=itemId, which="comments")
    
    # Add a rating of an item into the system
    def addItemMetaRating(self, rating, rating_type, item=None, itemId=None):
        """ Adds a rating for the specified item """
        ratings = list(self._metaratings)
        if item and not itemId:
            itemId = "%f:%f:%s" % (item.get('lat', "n"), item.get('lon', "n"), item.get('item_type', "n"))
        
        if itemId and not item:
            items = itemId.split(":")
            item = {'lat':items[0], 'lon':items[1], 'item_type':items[2]}
        
        rId = itemId
        # If ANY stuff was not passed or passed incorrectly return false 
        if "n" in rId.split(":") or rating not in ["up", "down"]:
            return False
        
        found = False
        for idx, r in enumerate([x["id"] for x in ratings]):
            if rId == r and not found:
                print "Already have a ratings object"
                # Item already has a ratings object
                found = True
                obj = ratings[idx]["obj"]
                if rating_type == "acc":
                    obj.acc_rating[rating] = obj.acc_rating[rating] + 1
                    import transaction
                    transaction.commit()
                    return obj.getFormattedRatings()
                elif rating_type == "nice":
                    obj.nice_rating[rating] = obj.nice_rating[rating] + 1
                    import transaction
                    transaction.commit()
                    return obj.getFormattedRatings()
                else:
                    # Something is wrong
                    print "Couldn't update rating"
                    return False
        
        if not found:
            # Item was not found, add it to the store
            print "Item was not present in the system"
            obj = ItemMetaRating(item["item_type"], item["lat"], item["lon"])
            if rating_type == "acc":
                obj.acc_rating[rating] = obj.acc_rating[rating] + 1
            elif rating_type == "nice":
                obj.nice_rating[rating] = obj.nice_rating[rating] + 1
            else:
                # Something is wrong
                print "Couldn't update rating"
                return False
            ratings.append({'id':rId, 'obj':obj})
            self._metaratings = tuple(ratings)
            import transaction
            transaction.commit()
            return obj.getFormattedRatings()
            # return a formatted dict of the result because of the AJAX add method
            
    
    # Add a comment on an item into the system
    def addItemMetaComment(self, item, comment):
        """ Adds a comment for the specified item """
        comments = list(self._metacomments)
        rId = "%f:%f:%s" % (item.get('lat', "n"), item.get('lon', "n"), item.get('item_type', "n"))
        # If ANY stuff was not passed or passed incorrectly return false 
        if "n" in rId.split(":"):
            return False
        
        found = False
        for idx, r in enumerate([x["id"] for x in comments]):
            if rId == r:
                print "Already have a ratings object"
                # Item already has a ratings object
                found = True
                obj = comments[idx]["obj"]
                obj.addComment(comment)
        
        if not found:
            # Item was not found, add it to the store
            print "Item was not present in the system"
            obj = ItemMetaComments(item["item_type"], item["lat"], item["lon"])
            obj.addComment(comment)
        
        self._metacomments = tuple(comments)
        import transaction
        transaction.commit()
    

def appmaker(zodb_root):
    if not 'app_root' in zodb_root:
        app_root = MyModel()
        app_root._initialiseSite()
        zodb_root['app_root'] = app_root
        import transaction
        transaction.commit()
    return zodb_root['app_root']
